/*
    S3Tris, a game (like TETRIS(TM)) for the Psion Series 3
    Version 1.1a
    Copyright (C) 1992, 93  J Cade Roux

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

 * (TETRIS is a trademark of AcademySoft-ELORG)
 *
 * See the file COPYING for the GNU General Public License.
 * Mail: Cade Roux
 *                      c/o Dubroca
 *                      Box 513
 *                      Boutte, LA  70039
 *                      USA
 */

#include <plib.h>
#include <wlib.h>
#include <hwif.h>

#include <utils.h>

#include "global.h"
#include "s3tris.h"

#define NUM_TOP_SCORES		5
#define LAST_TOP_SCORE		(NUM_TOP_SCORES - 1)
#define TOP_NAME_LEN			12
#define TOP_FILE_SIZE		(NUM_TOP_SCORES * sizeof(TOP_SCORES))
#define TEXT_LEADING			1
#define N_REL					3

#define FIRST_PIECE			0
#define LAST_PIECE			6
#define PIECE_SQR				0
#define PIECE_THN				5
#define NUM_GFX				5
#define CHARSET_START		1

#define SMALLEST_SIZE		4
#define LARGEST_SIZE			8
#define SMALLEST_WIDTH		6
#define SMALLEST_HEIGHT		6

#define STATUS_WIDTH			50
#define IMAGE_WIDTH			24
#define IMAGE_HEIGHT			24

#define N_KEYS					6

#define BASE_TIMER_INTERVAL		6
#define MIN_TIMER_INTERVAL			2
#define LOWEST_LEVEL					0
#define HIGHEST_LEVEL				9


/* Top scores table structure */
typedef struct {
	TEXT		name[TOP_NAME_LEN] ;
	UINT		score ;
} TOP_SCORES ;

/* Configuration structure */
typedef struct {
	UWORD			sound_fx ;		/* sound */
	UWORD			orient ;			/* across? */
	UWORD			preview ;		/* next piece */
	UWORD			fill_board ;	/* random fill */

	UWORD			keys[N_KEYS] ;

	UWORD			size ;			/* block size in pixels */

	BYTE 			fill_lines ;	/* # Lines to fill */
	BYTE			fill_factor ;	/* Fill factor (0-100) */
	BYTE			maxx ;			/* highest valid x position on playfield */
	BYTE			maxy ;			/* highest valid y position on playfield */
} CONFIG ;

/* Relative positions of 3 components */
typedef struct {
	BYTE		dx[N_REL] ;
	BYTE		dy[N_REL] ;
} RELPOS ;

GLREF_D WSERV_SPEC *		wserv_channel ;
GLREF_D VOID *				DatCommandPtr ;

GLDEF_D TEXT **			_cmds ;
GLDEF_D H_MENU_DATA *	_mdata ;

LOCAL_D TEXT *		S3TRIS_SCORES = "LOC::M:\\APP\\TMP\\S3TRIS.TOP" ;
LOCAL_D TEXT *		S3TRIS_CONFIG = "LOC::M:\\APP\\TMP\\S3TRIS.CFG" ;

/* General graphics data required by S3Tris */
LOCAL_D struct {
	UINT	win ;				/* main window */
	INT	gc ;				/* plain text gc - for status win */
	INT	gc_draw ;		/* piece drawing gc */
	INT	gc_erase ;		/* piece erasing gc */
	INT	bmp ;				/* bitmap gfx data */
	INT	font ;			/* custom font id */
} gfx ;

#define SOUND_OFF		1
#define SOUND_ON		2
#define ORIENT_VERT	1
#define ORIENT_HORZ	2
#define PREVIEW_OFF	1
#define PREVIEW_ON	2
#define FILL_OFF		1
#define FILL_ON		2

/* Config initialized to defaults */
LOCAL_D CONFIG config = {	SOUND_ON, ORIENT_VERT, PREVIEW_ON, FILL_OFF,
									W_KEY_LEFT, W_KEY_RIGHT, W_KEY_DOWN, W_KEY_UP, '.', ' ',
									7, 4, 40, 10, 19 } ;

#define OPT_HORZ				( config.orient == ORIENT_HORZ )
#define OPT_VERT				( config.orient == ORIENT_VERT )
#define OPT_SOUND				( config.sound_fx == SOUND_ON )
#define OPT_FILL				( config.fill_board == FILL_ON )
#define OPT_PREVIEW			( config.preview == PREVIEW_ON )

/* Game status - one instance */
LOCAL_D struct {
	VOID *	ptcb ;				/* Pointer to timer control block        */

	UBYTE		cx ;					/* Current position of central component */
	UBYTE		cy ;

	RELPOS	relpos ;				/* Relative position of other components */

	UINT		cpiece ;				/* Current piece (0-6)                   */
	UINT		npiece ;				/* Next piece (0-6)                      */

	UINT		level ;				/* Level (0-9)                           */
	UINT		score ;				/* Score (0-65535)                       */
	UINT		lines ;				/* Lines (0-65535)                       */

	TEXT		ch ;					/* Character code for current piece      */

	WORD		timer_status ;		/* Timer status word                     */
	ULONG 	timer_interval ;	/* Timer interval                        */

	ULONG		ulseed ;				/* Random number seed                    */

	BOOL		restart ;			/* Flag to restart game                  */
	BOOL		stop ;				/* Flag to stop game                     */
	BOOL		quit ;				/* Flag to quit game                     */
} status = { NULL } ;

LOCAL_D struct {
	P_POINT		screen_dim ;
	P_POINT		prev ;
	UWORD			text_height ;
	UWORD			text_ascent ;
} metrics ;

/* Main and playing menus */
#define MAIN_GAME_ITEMS					3
#define MAIN_CONFIG_ITEMS				3

#define MAIN_ITEMS_BEFORE_GAME		0
#define MAIN_ITEMS_BEFORE_CONFIG		MAIN_GAME_ITEMS

LOCAL_D H_MENU_DATA		main_mdata[] = {	"Game", MAIN_GAME_ITEMS,
														"Config", MAIN_CONFIG_ITEMS,
														NULL } ;

LOCAL_D TEXT *				main_cmds[] = {	"pPlay",
														"tTop Scores",
														"xExit",
														"oOpen",
														"eEdit",
														"sSave",
														NULL } ;

#define PLAY_S3TRIS_ITEMS				6

#define PLAY_ITEMS_BEFORE_S3TRIS		0

LOCAL_D H_MENU_DATA 		playing_mdata[] = {	"S3Tris", PLAY_S3TRIS_ITEMS,
															NULL } ;

LOCAL_D TEXT *				playing_cmds[] = {	"oOptions",
															"kKeys",
															"pPause Game",
															"sStop Game",
															"nNew Game",
															"xExit",
															NULL } ;

/* Beep if sound fx are turned on */
LOCAL_C VOID Beep(UINT x, UINT y) {
	if ( OPT_SOUND )
		p_sound(x, y) ;
}

/* Check to see if the key is a movement key */
LOCAL_C INT CheckForMovementKey(INT keycode) {
	INT		iLp ;

	for ( iLp = 0 ; iLp < N_KEYS ; iLp++ ) {
		if ( keycode == config.keys[iLp] )
			return(iLp) ;
	}

	return(-1) ;
}

/* Convert the UINT uinum to a ZTS string at str */
LOCAL_C TEXT *DecimalStr(TEXT *str, UINT uinum) {
	str[p_gtob(str, uinum, 10)] = '\x0' ;

	return(str) ;
}

/* Create and initialize a window centered on the screen */
LOCAL_C INT NewWindow(WORD width, WORD height) {
	W_WINDATA		windata = {	0,
										{{0, 0}, 0, 0},
										0,
										W_WIN_BACK_BITMAP } ;
	INT				win ;

	windata.extent.tl.x = (metrics.screen_dim.x - width) / 2 ;
	windata.extent.tl.y = (metrics.screen_dim.y - height) / 2,
	windata.extent.width = width ;
	windata.extent.height = height ;
	win = wCreateWindow(	gfx.win,
								W_WIN_EXTENT | W_WIN_BACKGROUND,
								&windata,
								(UWORD)&gfx) ;
	wInitialiseWindowTree(win) ;

	return(win) ;
}

/* Pause game */
LOCAL_C VOID Pause(VOID) {
	P_RECT			rect ;
	INT				win ;
	WMSG_KEY			key ;
	TEXT *			str = NULL ;

	ReadResource(RS_PAUSED, &str) ;

	rect.br.x = 8 + gTextWidth(WS_FONT_SYSTEM, G_STY_NORMAL, str, p_slen(str)) ;
	rect.br.y = 6 + metrics.text_height ;

	win = NewWindow(rect.br.x, rect.br.y) ;

/* Start temp GC */
	gCreateTempGC0(win) ;
		gBorder(W_BORD_SHADOW_ON | W_BORD_SHADOW_D) ;

		rect.tl.x = 3 ;
		rect.tl.y = 2 ;
		rect.br.x -= 5 ;
		rect.br.y -= 4 ;
		gPrintBoxText(	&rect,
							metrics.text_ascent,
							G_TEXT_ALIGN_CENTRE,
							0,
							str,
							p_slen(str) ) ;
	gFreeTempGC() ;
/* End temp GC */

	p_free(str) ;

	for ( ; ; ) {
		uGetKey(&key) ;
		if ( key.keycode == CONS_EVENT_COMMAND )
			p_exit(E_APP_NONE) ;
		else if ( key.keycode == W_KEY_ESCAPE )
			break ;
	}

	wCloseWindowTree(win) ;
}

/*	Prompt for a key, leaving the key code in the word at pcode if Escape
	was not pressed.  Returns FALSE if Escape was pressed, TRUE otherwise
*/
LOCAL_C BOOL PromptKey(INT rs_prompt, UWORD *pcode) {
	INT				win ;
	P_RECT			rect ;
	WMSG_KEY			key ;
	WORD				width ;
	WORD				height ;
	TEXT *			str1 = NULL ;
	TEXT *			str2 = NULL ;

	ReadResource(RS_ESCAPE, &str1) ;
	ReadResource(rs_prompt, &str2) ;

	width = 7 + MAX(	gTextWidth(WS_FONT_SYSTEM, G_STY_NORMAL, str1, p_slen(str1)),
							gTextWidth(WS_FONT_SYSTEM, G_STY_NORMAL, str2, p_slen(str2)) ) ;
	height = 9 + 2 * metrics.text_height ;

	win = NewWindow(width, height) ;

/* Start temp GC */	
	gCreateTempGC0(win) ;
		gBorder(W_BORD_SHADOW_S | W_BORD_SHADOW_ON) ;

		rect.tl.x = 3 ;
		rect.tl.y = 2 ;
		rect.br.x = width - 4 ;
		rect.br.y = rect.tl.y + metrics.text_height ;
		gPrintBoxText(	&rect,
							metrics.text_ascent,
							G_TEXT_ALIGN_CENTRE,
							0,
							str1,
							p_slen(str1)) ;

		rect.br.y = height - 3 ;
		rect.tl.y = rect.br.y - metrics.text_height ;
		gPrintBoxText(	&rect,
							metrics.text_ascent,
							G_TEXT_ALIGN_CENTRE,
							0,
							str2,
							p_slen(str2)) ;

		gDrawLine(0, height / 2, width, height / 2) ;
	gFreeTempGC() ;
/* End temp GC */

	p_free(str1) ;
	p_free(str2) ;

	for ( ; ; ) {

		uGetKey(&key) ;

		switch (key.keycode) {

			case CONS_EVENT_COMMAND :
				p_exit(E_APP_NONE) ;

			case CONS_EVENT_FOREGROUND :
			case CONS_EVENT_BACKGROUND :
			case CONS_EVENT_ON_OFF :
				break ;

			case W_KEY_ESCAPE :
				wCloseWindowTree(win) ;
				InfoMsg(RS_ABORTED) ;
				return(FALSE) ;

			default :
				wCloseWindowTree(win) ;
				*pcode = key.keycode ;
				InfoMsg(RS_KEY_READ) ;
				return(TRUE) ;
		}
	}
}

/* Read in the config in filename */
LOCAL_C VOID ReadCfg(TEXT *filename) {
	VOID *		pfcb = NULL ;

	if (  p_open(&pfcb, filename, P_FSTREAM | P_FOPEN) ||
			p_read(pfcb, &config, sizeof(CONFIG)) != sizeof(CONFIG) ||
			p_close(pfcb) )
		Exit_Error(RS_ERR_READING_SCR) ;
}

/* Write the current config to filename */
LOCAL_C VOID WriteCfg(TEXT *filename) {
	VOID *		pfcb = NULL ;

	hEnsurePath(filename) ;
	if (	p_open(&pfcb, filename, P_FSTREAM | P_FUPDATE | P_FREPLACE) ||
			p_write(pfcb, &config, sizeof(CONFIG)) ||
			p_close(pfcb) )
		Exit_Error(RS_ERR_WRITING_CFG) ;
}

/* Read top scores file into an array at ptop_scores */
LOCAL_C BOOL ReadScores(TOP_SCORES *ptop_scores) {
	VOID *		pfcb = NULL ;

	if ( !Exists(S3TRIS_SCORES) )
		return(FALSE) ;
	if (  p_open(&pfcb, S3TRIS_SCORES, P_FSTREAM | P_FOPEN) ||
			p_read(pfcb, ptop_scores, TOP_FILE_SIZE) != TOP_FILE_SIZE ||
			p_close(pfcb) )
		Exit_Error(RS_ERR_READING_SCR) ;
	return(TRUE) ;
}

/* Write top scores file from array at ptop_scores */
LOCAL_C VOID WriteScores(TOP_SCORES *ptop_scores) {
	VOID *		pfcb = NULL ;

	hEnsurePath(S3TRIS_SCORES) ;
	if (  p_open(&pfcb, S3TRIS_SCORES, P_FSTREAM | P_FUPDATE | P_FREPLACE) ||
			p_write(pfcb, ptop_scores, TOP_FILE_SIZE) ||
			p_close(pfcb) )
		Exit_Error(RS_ERR_WRITING_SCR) ;
}

/* Show the score table if it exists */
LOCAL_C VOID ShowTopScores(VOID) {
	INT					iLp ;
	TOP_SCORES			top_scores[NUM_TOP_SCORES] ;
	TEXT					strings[NUM_TOP_SCORES][TOP_NAME_LEN + 12] ;

	if ( ReadScores(top_scores) ) {
		for (	iLp = 0 ;
				iLp < NUM_TOP_SCORES ;
				iLp++ )
			p_scatm(	DecimalStr(strings[iLp], top_scores[iLp].score),
						" - ",
						BCStoZTS(top_scores[iLp].name),
						NULL ) ;

		uDisplayText(	strings[0],
							strings[1],
							strings[2],
							strings[3],
							strings[4],
							NULL ) ;
	}
	else
		InfoMsg(RS_NO_TOP_SCORES) ;
}

#define CF_SAVE 's'
#define CF_OPEN 'o'

/* Prompt for saving/opening config files */
LOCAL_C VOID ConfigFile(char cf_flag) {
	TEXT				cfg[P_FNAMESIZE] ;
	H_DI_FSEL		fsel = { NULL } ;
	INT				choice = 0 ;

	p_scpy(cfg, S3TRIS_CONFIG) ;
	Check( p_enter6(ThreeQuery, cf_flag == CF_SAVE ? RS_SAVE_AS : RS_OPEN, RS_YES, RS_NO, RS_CHOOSE, &choice) ) ;
	switch (choice) {

		case 1 :
			fsel.flags = H_FILE_RESTRICT_LIST | ( ( cf_flag == CF_SAVE ) * H_FILE_NEW_EDITOR ) ;
			fsel.fname = ZTStoBCS(cfg) ;
			if ( uOpenDialog(NULL) )
				return ;
			Res_AddDialogItem(H_DIALOG_FSEL, RS_CONFIG_FILE, &fsel) ;
			if( Res_RunDialog() == 0 )
				return ;
			BCStoZTS(cfg) ;
			/* Fall through */

		case 2 :
			(cf_flag == CF_SAVE ? WriteCfg : ReadCfg)(cfg) ;
			/* Fall through */

		default :
			break ;
	}
}

/* Update the piece preview */
LOCAL_C VOID Put_Preview(VOID) {
	P_RECT		rect = {	{0, 0},
								{IMAGE_WIDTH, IMAGE_HEIGHT} } ;

	if ( OPT_PREVIEW )
		p_offrec(	&rect,
						IMAGE_WIDTH * status.npiece,
						IMAGE_HEIGHT * (OPT_HORZ || !status.npiece) ) ;

	gCopyBit(&metrics.prev, gfx.bmp, &rect, G_TRMODE_REPL) ;
}

/* For editing options during play only */
LOCAL_C VOID Edit_Options(VOID) {
	if ( uOpenDialog(NULL) )
		return ;
	Res_AddChoiceList(RS_SOUND_FX, RS_OFF, RS_ON, &config.sound_fx) ;
	Res_AddChoiceList(RS_NEXT_PIECE, RS_OFF, RS_ON, &config.preview) ;
	if ( Res_RunDialog() ) {
		ConfigFile(CF_SAVE) ;
		Put_Preview() ;
	}
}

/* For editing all the keys */
LOCAL_C VOID Edit_Keys(VOID) {
	INT		iLp ;

	for (	iLp = 0 ;
			iLp < N_KEYS ;
			iLp++ ) {
		if (!PromptKey(RS_LEFT + iLp, &config.keys[iLp]))
			return ;
	}

	ConfigFile(CF_SAVE) ;
}

/* Edit the config */
LOCAL_C VOID Edit_Config(VOID) {
	UWORD				osize ;
	UWORD				ohorz ;
	LONG				tl1 ;
	LONG				tl2 ;
	H_DI_NUMBER		num1 ;
	H_DI_NUMBER		num2 ;
	INT				ret ;
	TEXT *			str = NULL ;

	/* Remember old orientation and size */
	ohorz = config.orient ;
	osize = config.size ;

	/* Set choices */
	config.size -= 3 ;

	ret = uOpenDialog(NULL) ||
			uAddChoiceList(	ReadResource(RS_SIZE, &str),
									&config.size,
									"4", "5", "6", "7", "8",
									NULL ) ;
	Res_AddChoiceList(RS_SOUND_FX, RS_OFF, RS_ON, &config.sound_fx) ;
	Res_AddChoiceList(RS_NEXT_PIECE, RS_OFF, RS_ON, &config.preview) ;
	Res_AddChoiceList(RS_ORIENT, RS_VERT, RS_HORZ, &config.orient) ;
	Res_AddChoiceList(RS_FILL_BOARD, RS_NO, RS_YES, &config.fill_board) ;
	ret = ret || !Res_RunDialog() ;

	p_free(str) ;
	config.size += 3 ;

	if ( ret )
		return ;

	/* Set number dialogs */
	if ( ohorz == config.orient && osize == config.size ) {
		tl1 = config.maxx + 1 ;
		tl2 = config.maxy + 1 ;
	}
	else {
		tl2 = (metrics.screen_dim.y - 2) / config.size ;
		if ( OPT_HORZ )
			tl1 = (metrics.screen_dim.x - STATUS_WIDTH - 2) / config.size ;
		else
			tl1 = (metrics.screen_dim.y - 2) / (2 * config.size) ;
	}

	num1.value = &tl1 ;
	num1.low = SMALLEST_WIDTH ;
	num1.high = (metrics.screen_dim.x - STATUS_WIDTH - 2) / config.size ;

	num2.value = &tl2 ;
	num2.low = SMALLEST_HEIGHT ;
	num2.high = (metrics.screen_dim.y - 2) / config.size ;

	Res_OpenDialog(RS_BOARD_SIZE) ;
	Res_AddDialogItem(H_DIALOG_NUMBER, RS_HORZ, &num1) ;
	Res_AddDialogItem(H_DIALOG_NUMBER, RS_VERT, &num2) ;
	if ( !Res_RunDialog() )
		return ;

	config.maxx = tl1 - 1 ;
	config.maxy = tl2 - 1 ;

	if ( OPT_FILL ) {
		num1.low = 1 ;
		num1.high = ( OPT_HORZ ? config.maxx : config.maxy ) ;
		tl1 = config.fill_lines > num1.high ? num1.high / 2 : config.fill_lines ;
		num1.value = &tl1 ;

		num2.low = 1 ;
		num2.high = 99 ;
		tl2 = config.fill_factor ;
		num2.value = &tl2 ;

		Res_OpenDialog(RS_FILL) ;
		Res_AddDialogItem(H_DIALOG_NUMBER, RS_ROWS, &num1) ;
		Res_AddDialogItem(H_DIALOG_NUMBER, RS_FILL_FACTOR, &num2) ;
		if ( !Res_RunDialog() )
			return ;

		config.fill_lines = tl1 ;
		config.fill_factor = tl2 ;
	}

	Edit_Keys() ;
}

/* Print text str on line with align alignment */
LOCAL_C VOID StatusText(INT line, UINT align, TEXT *str) {
	P_RECT			rect ;

	rect.tl.x = metrics.screen_dim.x - STATUS_WIDTH ;
	rect.tl.y = 1 + line * (metrics.text_ascent + TEXT_LEADING) ;
	rect.br.x = metrics.screen_dim.x ;
	rect.br.y = rect.tl.y + metrics.text_ascent ;
	p_insrec(&rect, 2, 0) ;
	gPrintBoxText(&rect, metrics.text_ascent, align, 0, str, p_slen(str)) ;
}

/* Return TRUE if there's an error or if the user presses escape */
LOCAL_C BOOL GetName(TEXT *name_bcs) {
	TEXT				score_str[8] ;
	H_DI_EDIT		edit = { NULL, TOP_NAME_LEN - 1 } ;
	INT				ret ;
	TEXT *			str = NULL ;

	edit.str = name_bcs ;
	ReadResource(RS_GET_NAME, &str) ;

	do {
		ret =	uOpenDialog(str) ||
				uAddDialogItem(	H_DIALOG_EDIT,
										DecimalStr(score_str, status.score),
										&edit) ||
				uRunDialog() <= 0 ;
	} while ( name_bcs[0] == 0 && ret == FALSE ) ;

	p_free(str) ;

	return(ret) ;
}

/* Add the current score to the highscore table */
LOCAL_C VOID Add_Score(VOID) {
	INT				iLp ;
	INT				jLp ;
	TOP_SCORES		top_scores[NUM_TOP_SCORES] ;

	if ( !ReadScores(top_scores) ) {
		for (	iLp = 0 ;
				iLp < NUM_TOP_SCORES ;
				iLp++ ) {
			top_scores[iLp].name[0] = '\x0' ;
			top_scores[iLp].score = 0 ;
		}
	}
	else if ( status.score <= top_scores[LAST_TOP_SCORE].score ) {
		ShowTopScores() ;
		return ;
	}

	/* Find position in table to insert, iLp = position to put new score */
	for (	iLp = 0 ;
			iLp < NUM_TOP_SCORES && status.score <= top_scores[iLp].score ;
			iLp += status.score <= top_scores[iLp].score ) ;

	/* Move everything down one */
	for (	jLp = LAST_TOP_SCORE ;
			jLp > iLp ;
			top_scores[jLp].score = top_scores[jLp-1].score,
			p_bcpy(top_scores[jLp].name, top_scores[jLp-1].name, TOP_NAME_LEN),
			jLp-- ) ;

	/* Insert new score at iLp position */
	top_scores[iLp].name[0] = '\x0' ;
	if ( GetName(top_scores[iLp].name) )
		return ;
	top_scores[iLp].score = status.score ;
	
	/* Write out a new file over the old one */
	WriteScores(top_scores) ;
	ShowTopScores() ;
}

/* Reinitialize all variables for a new game */
LOCAL_C VOID Restart(VOID) {
	P_RECT		rect = { {0, 0} } ;
	INT			iLp ;
	INT			jLp ;
	INT			max_across ;
	TEXT *		str = NULL ;

	rect.br = metrics.screen_dim ;

	/* Re-initialize state */
	status.stop = status.restart = FALSE ;
	status.score = status.lines = 0 ;
	status.level = LOWEST_LEVEL ;
	status.timer_interval = BASE_TIMER_INTERVAL ;

	/* Clear screen */
	gClrRect(&rect, G_TRMODE_CLR) ;

	/* Box playfield */
	rect.br.x = 2 + config.size * (config.maxx + 1) ;
	rect.br.y = 2 + config.size * (config.maxy + 1) ;
	gDrawBox(&rect) ;

	rect.tl.x = metrics.screen_dim.x - STATUS_WIDTH ;
	rect.br.x = metrics.screen_dim.x ;
	rect.br.y = metrics.screen_dim.y ;
	gDrawBox(&rect) ;

	StatusText(0, G_TEXT_ALIGN_LEFT, ReadResource(RS_LINES, &str)) ;
	StatusText(2, G_TEXT_ALIGN_LEFT, ReadResource(RS_SCORE, &str)) ;
	StatusText(4, G_TEXT_ALIGN_LEFT, ReadResource(RS_LEVEL, &str)) ;
	p_free(str) ;

	status.ulseed = p_date() ;
	status.npiece = (UINT) Random(&status.ulseed, FIRST_PIECE, LAST_PIECE) ;

	if (	OPT_FILL &&
			config.fill_lines > 0 ) {
		max_across = ( OPT_HORZ ? config.maxy : config.maxx ) ;
		gSetGC0(gfx.gc_draw) ; /* Use drawing context with pieces font */

		for ( iLp = 1 ; iLp <= config.fill_lines ; iLp++ ) {
			for ( jLp = 0 ; jLp <= max_across ; jLp++ ) {
				if ( Random(&status.ulseed, 0, 100) <= config.fill_factor ) {
					/* Put block */
					status.ch = CHARSET_START +
									NUM_GFX * (config.size - SMALLEST_SIZE) +
									Random(&status.ulseed, 0, NUM_GFX - 1) ;
					if ( OPT_HORZ )
						gPrintText( 1 + config.size * (config.maxx + 1 - iLp), 1 + config.size * (jLp + 1), &status.ch, 1) ;
					else
						gPrintText( 1 + config.size * jLp, 1 + config.size * (config.maxy + 2 - iLp), &status.ch, 1) ;
				}
			}
		}

		gSetGC0(gfx.gc) ;
	}
}

#define ES_SCORE 's'
#define ES_DONT_SCORE 'n'

/* Exit, allowing a score to be entered if so desired */
LOCAL_C VOID Exit_S3Tris(char es_flag) {
	Check( p_enter5(Query, RS_QUIT, RS_YES, RS_NO, &status.quit) ) ;
	if ( !status.quit )
		return ;

	if ( es_flag == ES_SCORE )
		Add_Score() ;

	p_exit(E_APP_NONE) ;
}

/* Execute a command on the Playing menu */
LOCAL_C VOID ExecutePlayCommand(INT keycode) {
	keycode = uLocateCommand(keycode) ;
	if ( keycode >= 0 )
		switch ( keycode ) {
			case PLAY_ITEMS_BEFORE_S3TRIS :
				/* Options */
				Edit_Options() ;
				break ;

			case PLAY_ITEMS_BEFORE_S3TRIS + 1 :
				/* Keys */
				Edit_Keys() ;
				break ;

			case PLAY_ITEMS_BEFORE_S3TRIS + 2 :
				/* Pause */
				Pause() ;
				break ;

			case PLAY_ITEMS_BEFORE_S3TRIS + 3 :
				/* Stop  - return to main menu */
				Check( p_enter5(Query, RS_STOP, RS_YES, RS_NO, &status.stop) ) ;
				break ;

			case PLAY_ITEMS_BEFORE_S3TRIS + 4 :
				/* New game */
				Check( p_enter5(Query, RS_NEW_GAME, RS_YES, RS_NO, &status.restart) ) ;
				break ;

			case PLAY_ITEMS_BEFORE_S3TRIS + 5 :
				/* Quit */
				Exit_S3Tris(ES_SCORE) ;
				break ;

			default :
				break ;
		}
}

#define DP_DRAW 'd'
#define DP_ERASE 'e'

/* Draw or erase the piece at the current position */
LOCAL_C VOID Draw_Piece(char dp_flag) {
	INT		iLp ;
	INT		xDisp ;
	INT		yDisp ;

	xDisp = 1 + status.cx ;
	yDisp = 1 + config.size + status.cy ;
	gSetGC0(dp_flag == DP_DRAW ? gfx.gc_draw : gfx.gc_erase) ;
	gPrintText(xDisp, yDisp, &status.ch, 1) ;
	for (	iLp = 0 ;
			iLp < N_REL ;
			iLp++ )
		gPrintText(xDisp + status.relpos.dx[iLp], yDisp + status.relpos.dy[iLp], &status.ch, 1) ;

	gSetGC0(gfx.gc) ;
}

/* TRUE If current PIECE Position free, FALSE Out of bounds or occupied */
LOCAL_C BOOL PieceOK(VOID) {
	P_POINT base, start, max ;
	INT iLp ;
	UBYTE d[2] ;

	max.x = config.size * config.maxx ;
	max.y = config.size * config.maxy ;
	for (	iLp = 0 ;
			iLp < N_REL ;
			iLp++ ) {
		if (	status.cx + status.relpos.dx[iLp] < 0 ||
				status.cy + status.relpos.dy[iLp] < 0 ||
				status.cx + status.relpos.dx[iLp] > max.x ||
				status.cy + status.relpos.dy[iLp] > max.y )
			return(FALSE) ;
	}

	base.x = 1 + status.cx ;
	base.y = 1 + status.cy ;
	gPeekBit(gfx.win, &base, 1, d) ;
	if ( d[0] & 1 )
		return(FALSE) ;

	for (	iLp = 0 ;
			iLp < N_REL ;
			iLp++ ) {
		start.x = base.x + status.relpos.dx[iLp] ;
		start.y = base.y + status.relpos.dy[iLp] ;
		gPeekBit(gfx.win, &start, 1, d) ;
		if ( d[0] & 1 )
			return(FALSE) ;
	}
	return(TRUE) ;
}

#define DIRN_POS 1
#define DIRN_NEG -1

/* Move in the Y direction if possible */
LOCAL_C VOID MoveY(INT dirn) {
	status.cy += dirn * config.size ;
	if ( !PieceOK() ) {
		Beep(2, 300) ;
		status.cy -= dirn * config.size ;
	}
}

/* Move in the X direction if possible */
LOCAL_C VOID MoveX(INT dirn) {
	status.cx += dirn * config.size ;
	if ( !PieceOK() ) {
		Beep(2, 300) ;
		status.cx -= dirn * config.size ;
	}
}

/* Rotate in the +ve direction */
LOCAL_C VOID RotatePos(VOID) {
	INT	iLp ;
	BYTE	temp ;

	for (	iLp = 0 ;
			iLp < N_REL ;
			iLp++ ) {
		temp = status.relpos.dy[iLp] ;
		status.relpos.dy[iLp] = -status.relpos.dx[iLp] ;
		status.relpos.dx[iLp] = temp ;
	}
}

/* Rotate in the -ve direction */
LOCAL_C VOID RotateNeg(VOID) {
	INT	iLp ;
	BYTE	temp ;

	for (	iLp = 0 ;
			iLp < N_REL ;
			iLp++ ) {
		temp = status.relpos.dx[iLp] ;
		status.relpos.dx[iLp] = -status.relpos.dy[iLp] ;
		status.relpos.dy[iLp] = temp ;
	}
}

/* Rotate piece if possible */
LOCAL_C VOID RotatePiece(BOOL rot_pos) {
	Draw_Piece(DP_ERASE) ;
	(rot_pos ? RotatePos : RotateNeg)() ;
	if ( !PieceOK() ) {
		Beep(2, 300) ;
		(rot_pos ? RotateNeg : RotatePos)() ;
	}
	Draw_Piece(DP_DRAW) ;
}

/* Remove any full rows, updating the score */
LOCAL_C VOID RemoveRows(VOID) {
	BYTE			max_height ;
	BYTE			max_width ;
	INT			iLp ;
	INT			jLp ;
	BOOL			remove_line ;
	P_RECT		rect ;
	UBYTE			d[2] ;
	P_POINT		point ;

	Beep(2, 400) ;
	max_height = ( OPT_HORZ ? config.maxx : config.maxy ) ;
	max_width = ( OPT_HORZ ? config.maxy : config.maxx ) ;

	for (	iLp = 0 ;
			iLp <= max_height ;
			iLp++ ) {

		for (	remove_line = TRUE, jLp = 0 ;
				remove_line && jLp <= max_width ;
				jLp++ ) {
			point.x = 1 + config.size * ( OPT_HORZ ? iLp : jLp ) ;
			point.y = 1 + config.size * ( OPT_HORZ ? jLp : iLp ) ;
			gPeekBit(gfx.win, &point, 1, d) ;
			remove_line = d[0] & 1 ;
		}

		if ( remove_line ) {
			status.lines++ ;
			status.score += (status.level + 1) * (max_height - iLp + 1) ;

			Beep(2, 500) ;
			if ( iLp > 0 ) {
				/* Scroll rectangle in window */
				point.x = (OPT_HORZ ? config.size : 0) ;
				point.y = (OPT_VERT ? config.size : 0) ;
				rect.tl.x = rect.tl.y = 1 ;
				rect.br.x = 1 + config.size * (OPT_HORZ ? iLp : 1 + config.maxx) ;
				rect.br.y = 1 + config.size * (OPT_VERT ? iLp : 1 + config.maxy) ;

				wScrollRect(gfx.win, &rect, &point) ;

			}

			/* Erase top line in window */
			rect.tl.x = rect.tl.y = 1 ;
			rect.br.x = 1 + config.size * (1 + OPT_VERT * config.maxx) ;
			rect.br.y = 1 + config.size * (1 + OPT_HORZ * config.maxy) ;
			gClrRect(&rect, G_TRMODE_CLR) ;

			status.level = MIN(status.lines / 10, HIGHEST_LEVEL) ;
			status.timer_interval = MAX(BASE_TIMER_INTERVAL - status.level / 2, MIN_TIMER_INTERVAL) ;
		}
	}
}

/* Empty the key buffer */
LOCAL_C VOID FlushKeyBuffer(VOID) {
	WMSG_KEY key ;

	while (uKeyPressOutstanding())
		uGetKey(&key) ;
}

/* Queue a timer */
LOCAL_C VOID QueueTimer(VOID) {
	if ( p_ioa(	status.ptcb,
					P_FRELATIVE,
					&status.timer_status,
					&status.timer_interval ) )
		Exit_Error(RS_ERR_QUEUE) ;
}

/* Cancel a timer */
LOCAL_C VOID CancelTimer(VOID) {
	INT		ret ;

	ret = p_iow(status.ptcb, P_FCANCEL) ;
	if (	ret != 0 &&
			ret != E_FILE_CANCEL )
		Exit_Error(RS_ERR_CANCEL) ;
	p_waitstat(&status.timer_status) ;
}

/* Handle a message from the system screen */
LOCAL_C VOID HandleSpecialEvent(UWORD keycode) {
	switch ( keycode ) {

		case CONS_EVENT_COMMAND :
			p_exit(E_APP_NONE) ;

		case CONS_EVENT_ON_OFF :
		case CONS_EVENT_BACKGROUND :
			Pause() ;
	}
}

/* Attempt to place a new piece */
LOCAL_C BOOL NewPiece(VOID) {
	/* Relative starting positions of components, horizontal and vertical play */
	static RELPOS	relpos_h[] = {	{ {-1,-1,0}, {0,1,1} },
											{ {-1,-1,0}, {1,0,-1} },
											{ {-1,-1,0}, {-1,0,1} },
											{ {0,-1,0}, {1,1,-1} },
											{ {0,-1,0}, {-1,-1,1} },
											{ {0,0,0}, {-1,1,2} },
											{ {-1,0,0}, {0,1,-1} } } ;
	static RELPOS	relpos_v[] = {	{ {0,1,1}, {-1,-1,0} },
											{ {-1,0,1}, {-1,-1,0} },
											{ {1,0,-1}, {-1,-1,0} },
											{ {-1,-1,1}, {0,-1,0} },
											{ {1,1,-1}, {0,-1,0} },
											{ {-1,1,2}, {0,0,0} },
											{ {0,1,-1}, {-1,0,0} } } ;
	INT				iLp ;
	TEXT				buf[10] ;

	status.cpiece = status.npiece ;
	status.npiece = (UINT) Random(&status.ulseed, FIRST_PIECE, LAST_PIECE) ;
	status.ch = CHARSET_START + NUM_GFX * (config.size - SMALLEST_SIZE) + Random(&status.ulseed, 0, NUM_GFX - 1) ;

	if ( OPT_HORZ ) {
		status.cx = status.cpiece != PIECE_THN ;
		status.cy = config.maxy / 2 ;
		status.relpos = relpos_h[status.cpiece] ;
	}
	else {
		status.cy = status.cpiece != PIECE_THN ;
		status.cx = config.maxx / 2 ;
		status.relpos = relpos_v[status.cpiece] ;
	}

	/* Scale up */
	status.cx *= config.size ;
	status.cy *= config.size ;
	for (	iLp = 0 ;
			iLp < N_REL ;
			iLp++ ) {
		status.relpos.dx[iLp] *= config.size ;
		status.relpos.dy[iLp] *= config.size ;
	}

	/* Return FALSE if a piece exists in any of the positions required
		for the new piece
	 */
	if ( !PieceOK() )
		return(FALSE) ;

	StatusText(1, G_TEXT_ALIGN_RIGHT, DecimalStr(buf, status.lines)) ;
	StatusText(3, G_TEXT_ALIGN_RIGHT, DecimalStr(buf, status.score)) ;
	StatusText(5, G_TEXT_ALIGN_RIGHT, DecimalStr(buf, status.level)) ;

	Put_Preview() ;
	return(TRUE) ;
}

/* Place a new piece and drop it */
LOCAL_C VOID DropPiece(VOID) {
	WMSG_KEY		key ;
	WORD			key_status ;
	BOOL			movex = FALSE ;
	BOOL			movey = FALSE ;
	INT			sense ;
	INT			ret ;

	status.stop = !NewPiece() ;
	if ( status.stop )
		return ;

	Draw_Piece(DP_DRAW) ;
	QueueTimer() ;
	uGetKeyA(&key_status, &key) ;

	for ( ; ; ) {
		p_iowait() ;		/* Wait for an event to happen */

		if ( status.timer_status != E_FILE_PENDING ) {
			/* Timer expired - drop piece one level */
			Draw_Piece(DP_ERASE) ;

			if ( OPT_HORZ )
				status.cx += config.size ;
			else
				status.cy += config.size ;

			if ( PieceOK() ) {
				Draw_Piece(DP_DRAW) ;
				wFlush() ;
				QueueTimer() ;
			}
			else {
				if ( OPT_HORZ )
					status.cx -= config.size ;
				else
					status.cy -= config.size ;
				Draw_Piece(DP_DRAW) ;
				RemoveRows() ;
				FlushKeyBuffer() ;
				return ;
			}

		}
		else {
			/* Key event occurred before expiration of timer */
			if ( key.keycode & W_EVENT_KEY )
				HandleSpecialEvent(key.keycode) ;
			else if ( key.keycode & W_SPECIAL_KEY )
				ExecutePlayCommand(key.keycode & ~W_SPECIAL_KEY) ;
			else if ( (ret = CheckForMovementKey(key.keycode)) != -1 ) {

				switch ( ret ) {

					case 0 :
						/* Left */
						if ( OPT_HORZ ) {
							movey = TRUE ;
							sense = DIRN_POS ;
						}
						else {
							movex = TRUE ;
							sense = DIRN_NEG ;
						}
						break ;

					case 1 :
						/* Right */
						if ( OPT_HORZ ) {
							movey = TRUE ;
							sense = DIRN_NEG ;
						}
						else {
							movex = TRUE ;
							sense = DIRN_POS ;
						}
						break ;

					case 2 :
						/* Pos Rot */
						if ( status.cpiece != PIECE_SQR )
							RotatePiece(TRUE) ;
						break ;

					case 3 :
						/* Neg Rot */
						if ( status.cpiece != PIECE_SQR )
							RotatePiece(FALSE) ;
						break ;

					case 4 :
						/* Down */
						if ( OPT_HORZ ) {
							movex = TRUE ;
							sense = DIRN_POS ;
						}
						else {
							movey = TRUE ;
							sense = DIRN_POS ;
						}
						break ;

					case 5 :
						/* Drop */
						CancelTimer() ;
						Draw_Piece(DP_ERASE) ;

						do {
							if ( OPT_HORZ )
								status.cx += config.size ;
							else
								status.cy += config.size ;
						} while ( PieceOK() ) ;

						if ( OPT_HORZ )
							status.cx -= config.size ;
						else
							status.cy -= config.size ;

						Draw_Piece(DP_DRAW) ;

						RemoveRows() ;
						FlushKeyBuffer() ;
						return ;

					default :
						break ;
				}

				if ( movex || movey ) {
					Draw_Piece(DP_ERASE) ;
					if ( movex ) {
						movex = FALSE ;
						MoveX(sense) ;
					}
					if ( movey ) {
						movey = FALSE ;
						MoveY(sense) ;
					}
					Draw_Piece(DP_DRAW) ;
				}

			}
			else
				switch ( key.keycode ) {

					case W_KEY_HELP :
						Help(RH_MAIN, RH_MAIN) ;
						break ;

					case W_KEY_ESCAPE :
						ExecutePlayCommand('s') ;
						break ;

					case W_KEY_MENU :
						if ( (ret = uPresentMenus()) > 0 )
							ExecutePlayCommand(ret) ;
						break ;

					default :
						break ;
				}

			if ( status.stop || status.restart )
				return ;

			uGetKeyA(&key_status, &key) ;
		}
	}
}

#pragma save, ENTER_CALL

/* Main menu, returns only when play has been selected */
LOCAL_C INT MainMenu(VOID) {
	INT			ret ;

	_cmds = main_cmds ;
	_mdata = main_mdata ;

	for ( ; ; ) {
		if ( (ret = uPresentMenus()) == 0 ) {
			Exit_S3Tris(ES_DONT_SCORE) ;
		}
		else switch ( uLocateCommand(ret) ) {
			case MAIN_ITEMS_BEFORE_GAME :
				/* Play */
				return ;

			case MAIN_ITEMS_BEFORE_GAME + 1 :
				ShowTopScores() ;
				break ;

			case MAIN_ITEMS_BEFORE_GAME + 2 :
				Exit_S3Tris(ES_DONT_SCORE) ;

			case MAIN_ITEMS_BEFORE_CONFIG :
				ConfigFile(CF_OPEN) ;
				break ;

			case MAIN_ITEMS_BEFORE_CONFIG + 1 :
				Edit_Config() ;
				break ;

			case MAIN_ITEMS_BEFORE_CONFIG + 2 :
				ConfigFile(CF_SAVE) ;
				break ;

			default :
				break ;
		}
	}

	return(E_APP_NONE) ;
}

LOCAL_C INT SpecificInit(VOID) {
	G_GC					g_gc ;
	G_FONT_INFO			font_info ;
	P_POINT				point ;

	hCrackCommandLine() ;
	p_unmarka() ;

	gfx.win = uFindMainWid() ;
	gfx.bmp = gOpenBit(DatCommandPtr, 0, 0, &point) ;
	gfx.font = gOpenFont(DatCommandPtr) ;
	OpenResource(DatCommandPtr) ;

	metrics.screen_dim = wserv_channel->conn.info.pixels ;
	metrics.prev.x = metrics.screen_dim.x - (STATUS_WIDTH + IMAGE_WIDTH) / 2 ;
	metrics.prev.y = metrics.screen_dim.y - IMAGE_HEIGHT - 4 ;

	gFontInfo(WS_FONT_SYSTEM, G_STY_NORMAL, &font_info) ;
	metrics.text_height = font_info.height ;
	metrics.text_ascent = font_info.ascent ;

	g_gc.font = gfx.font ;
	g_gc.textmode = G_TRMODE_SET ;
	gfx.gc_draw = gCreateGC(gfx.win, G_GC_MASK_TEXTMODE |	G_GC_MASK_FONT, &g_gc) ;

	g_gc.textmode = G_TRMODE_CLR ;
	gfx.gc_erase = gCreateGC(gfx.win, G_GC_MASK_TEXTMODE | G_GC_MASK_FONT, &g_gc) ;

	g_gc.font = WS_FONT_SYSTEM ;
	gfx.gc = gCreateGC(gfx.win, G_GC_MASK_TEXTMODE, &g_gc) ;

	Help(RH_ABOUT, RH_MAIN) ;

	f_open(&status.ptcb, "TIM:", -1) ;

	/* Read default config if it exists, else allow user to edit the config */
	if ( Exists(S3TRIS_CONFIG) )
		ReadCfg(S3TRIS_CONFIG) ;
	else
		Edit_Config() ;

	return(E_APP_NONE) ;
}

LOCAL_C INT MainLoop(VOID) {
	for ( ; ; ) {
		MainMenu() ;
		_cmds = playing_cmds ;
		_mdata = playing_mdata ;
		do {
			for (	Restart() ;
					!status.stop && !status.restart ;
					DropPiece() ) ;
			Add_Score() ;
		} while ( status.restart ) ;
	}

	return(E_APP_NONE) ;
}

#pragma restore

GLDEF_C VOID main(VOID) {
	uCommonInit() ;
	wDisableLeaves(FALSE) ;
	Check( p_enter1(SpecificInit) ) ;
	Check( p_enter1(MainLoop) ) ;
	p_exit(E_APP_NONE) ;
}
